<?php
namespace App\Support;

use Exception;

class MemcacheSASL
{
    const PREFIX = 'rfmj_';
    const PERIOD_PREFIX = 'rfmj_p_';
    protected $_request_format = 'CCnCCnNNNN';
    protected $_response_format = 'Cmagic/Copcode/nkeylength/Cextralength/Cdatatype/nstatus/Nbodylength/NOpaque/NCAS1/NCAS2';

    const OPT_COMPRESSION = -1001;

    const MEMC_VAL_TYPE_MASK = 0xf;
    const MEMC_VAL_IS_STRING = 0;
    const MEMC_VAL_IS_LONG = 1;
    const MEMC_VAL_IS_DOUBLE = 2;
    const MEMC_VAL_IS_BOOL = 3;
    const MEMC_VAL_IS_SERIALIZED = 4;

    const MEMC_VAL_COMPRESSED = 16; // 2^4

    protected function _build_request($data)
    {
        $valuelength = $extralength = $keylength = 0;
        if (array_key_exists('extra', $data)) {
            $extralength = strlen($data['extra']);
        }
        if (array_key_exists('key', $data)) {
            $keylength = strlen($data['key']);
        }
        if (array_key_exists('value', $data)) {
            $valuelength = strlen($data['value']);
        }
        $bodylength = $extralength + $keylength + $valuelength;
        $ret = pack($this->_request_format,
            0x80,
            $data['opcode'],
            $keylength,
            $extralength,
            array_key_exists('datatype', $data) ? $data['datatype'] : null,
            array_key_exists('status', $data) ? $data['status'] : null,
            $bodylength,
            array_key_exists('Opaque', $data) ? $data['Opaque'] : null,
            array_key_exists('CAS1', $data) ? $data['CAS1'] : null,
            array_key_exists('CAS2', $data) ? $data['CAS2'] : null
        );

        if (array_key_exists('extra', $data)) {
            $ret .= $data['extra'];
        }

        if (array_key_exists('key', $data)) {
            $ret .= $data['key'];
        }

        if (array_key_exists('value', $data)) {
            $ret .= $data['value'];
        }
        return $ret;
    }

    protected function _show_request($data)
    {
        $array = unpack($this->_response_format, $data);
        return $array;
    }

    protected function _send($data)
    {
        $send_data = $this->_build_request($data);
        fwrite($this->_fp, $send_data);
        return $send_data;
    }

    protected function _recv()
    {
        $data = fread($this->_fp, 24);
        $array = $this->_show_request($data);
        if ($array['bodylength']) {
            $bodylength = $array['bodylength'];
            $data = '';
            while ($bodylength > 0) {
                $recv_data = fread($this->_fp, $bodylength);
                $bodylength -= strlen($recv_data);
                $data .= $recv_data;
            }

            if ($array['extralength']) {
                $extra_unpacked = unpack('Nint', substr($data, 0, $array['extralength']));
                $array['extra'] = $extra_unpacked['int'];
            }
            $array['key'] = substr($data, $array['extralength'], $array['keylength']);
            $array['body'] = substr($data, $array['extralength'] + $array['keylength']);
        }
        return $array;
    }

    public function __construct()
    {
    }


    public function listMechanisms()
    {
        $this->_send(array('opcode' => 0x20));
        $data = $this->_recv();
        return explode(" ", $data['body']);
    }

    public function setSaslAuthData($user, $password)
    {
        $this->_send(array(
            'opcode' => 0x21,
            'key' => 'PLAIN',
            'value' => '' . chr(0) . $user . chr(0) . $password
        ));
        $data = $this->_recv();

        if ($data['status']) {
            throw new Exception($data['body'], $data['status']);
        }
    }

    public function addServer($host, $port, $weight = 0)
    {
        $this->_fp = stream_socket_client($host . ':' . $port);
    }

    public function addServers($servers)
    {
        for ($i = 0; $i < count($servers); $i++) {
            $s = $servers[$i];
            if (count($s) >= 2) {
                $this->addServer($s[0], $s[1]);
            } else {
                trigger_error("could not add entry #"
                    . ($i + 1) . " to the server list", E_USER_WARNING);
            }
        }
    }

    public function addServersByString($servers)
    {
        $servers = explode(",", $servers);
        for ($i = 0; $i < count($servers); $i++) {
            $servers[$i] = explode(":", $servers[$i]);
        }
        $this->addServers($servers);
    }

    public function get($key)
    {
        $sent = $this->_send(array(
            'opcode' => 0x00,
            'key' => self::PREFIX.$key,
        ));
        $data = $this->_recv();
        if (0 == $data['status']) {
            if (!isset($data['extra'])) {
                return false;
            }
            if (isset($data['extra']) && ($data['extra'] & self::MEMC_VAL_COMPRESSED)) {
                $body = gzuncompress($data['body']);
            } else {
                if (!isset($data['body'])) {
                    return false;
                }
                $body = isset($data['body']) ? $data['body'] : '';
            }

            $type = $data['extra'] & self::MEMC_VAL_TYPE_MASK;

            switch ($type) {
                case self::MEMC_VAL_IS_STRING:
                    $body = strval($body);
                    break;

                case self::MEMC_VAL_IS_LONG:
                    $body = intval($body);
                    break;

                case self::MEMC_VAL_IS_DOUBLE:
                    $body = doubleval($body);
                    break;

                case self::MEMC_VAL_IS_BOOL:
                    $body = $body ? true : false;
                    break;

                case self::MEMC_VAL_IS_SERIALIZED:
                    $body = unserialize($body);
                    break;
            }

            return $body;
        }
        return FALSE;
    }

    /**
     * process value and get flag
     *
     * @param int $flag
     * @param mixed $value
     * @access protected
     * @return array($flag, $processed_value)
     */
    protected function _processValue($flag, $value)
    {
        if (is_string($value)) {
            $flag |= self::MEMC_VAL_IS_STRING;
        } elseif (is_long($value)) {
            $flag |= self::MEMC_VAL_IS_LONG;
        } elseif (is_double($value)) {
            $flag |= self::MEMC_VAL_IS_DOUBLE;
        } elseif (is_bool($value)) {
            $flag |= self::MEMC_VAL_IS_BOOL;
        } else {
            $value = serialize($value);
            $flag |= self::MEMC_VAL_IS_SERIALIZED;
        }

        if (array_key_exists(self::OPT_COMPRESSION, $this->_options) and $this->_options[self::OPT_COMPRESSION]) {
            $flag |= self::MEMC_VAL_COMPRESSED;
            $value = gzcompress($value);
        }
        return array($flag, $value);
    }

    public function add($key, $value, $expiration = 0)
    {
        list($flag, $value) = $this->_processValue(0, $value);

        $extra = pack('NN', $flag, $expiration);
        $sent = $this->_send(array(
            'opcode' => 0x02,
            'key' => self::PREFIX.$key,
            'value' => $value,
            'extra' => $extra,
        ));
        $data = $this->_recv();
        if ($data['status'] == 0) {
            return TRUE;
        }

        return FALSE;
    }

    public function set($key, $value, $expiration = 0)
    {
        list($flag, $value) = $this->_processValue(0, $value);

        $extra = pack('NN', $flag, $expiration);
        $sent = $this->_send(array(
            'opcode' => 0x01,
            'key' => self::PREFIX.$key,
            'value' => $value,
            'extra' => $extra,
        ));
        $data = $this->_recv();
        if ($data['status'] == 0) {
            return TRUE;
        }

        return FALSE;
    }

    public function delete($key)
    {
        $sent = $this->_send(array(
            'opcode' => 0x04,
            'key' => self::PREFIX.$key,
        ));
        $data = $this->_recv();
        if ($data['status'] == 0) {
            return TRUE;
        }

        return FALSE;
    }

    public function replace($key, $value, $expiration = 0)
    {
        list($flag, $value) = $this->_processValue(0, $value);

        $extra = pack('NN', $flag, $expiration);
        $sent = $this->_send(array(
            'opcode' => 0x03,
            'key' => self::PREFIX.$key,
            'value' => $value,
            'extra' => $extra,
        ));
        $data = $this->_recv();
        if ($data['status'] == 0) {
            return TRUE;
        }

        return FALSE;
    }

    protected function _upper($num)
    {
        return $num << 32;
    }

    protected function _lower($num)
    {
        return $num % (2 << 32);
    }

    public function incrementRenci($goodsId, $increment = 1, $expiration = 0)
    {
        $key = self::PERIOD_PREFIX.$goodsId;
        $preriod = $this->get($key);
        $preriod['canyurenshu'] = $preriod['canyurenshu'] + $increment;
        $preriod['shenyurenshu'] = $preriod['shenyurenshu'] - $increment;
        if ($preriod['canyurenshu'] > $preriod['zongrenshu']) {
            return false;
        }
        if ($preriod['shenyurenshu'] < 0) {
            return false;
        }
        $value = $preriod;
        list($flag, $value) = $this->_processValue(0, $value);
        $extra = pack('NN', $flag, $expiration);
        $sent = $this->_send(array(
            'opcode' => 0x01,
            'key' => self::PREFIX.$key,
            'value' => $value,
            'extra' => $extra,
        ));
        $data = $this->_recv();
        if ($data['status'] == 0) {
            return TRUE;
        }
        return FALSE;
    }

    public function increment($key, $offset = 1, $expiration = 0)
    {
        $initial_value = 0;
        $extra = pack('N2N2N', $this->_upper($offset), $this->_lower($offset), $this->_upper($initial_value), $this->_lower($initial_value), $expiration);
        $sent = $this->_send(array(
            'opcode' => 0x05,
            'key' => self::PREFIX.$key,
            'extra' => $extra,
        ));
        $data = $this->_recv();
        if ($data['status'] == 0) {
            return TRUE;
        }

        return FALSE;
    }

    public function decrement($key, $offset = 1, $expiration = 0)
    {
        $initial_value = 0;
        $extra = pack('N2N2N', $this->_upper($offset), $this->_lower($offset), $this->_upper($initial_value), $this->_lower($initial_value), $expiration);
        $sent = $this->_send(array(
            'opcode' => 0x06,
            'key' => self::PREFIX.$key,
            'extra' => $extra,
        ));
        $data = $this->_recv();
        if ($data['status'] == 0) {
            return TRUE;
        }

        return FALSE;
    }

    /**
     * Get statistics of the server
     *
     * @param string $type The type of statistics to fetch. Valid values are
     *                     {reset, malloc, maps, cachedump, slabs, items,
     *                     sizes}. According to the memcached protocol spec
     *                     these additional arguments "are subject to change
     *                     for the convenience of memcache developers".
     *
     * @link http://code.google.com/p/memcached/wiki/BinaryProtocolRevamped#Stat
     * @access public
     * @return array  Returns an associative array of server statistics or
     *                FALSE on failure.
     */
    public function getStats($type = null)
    {
        $this->_send(
            array(
                'opcode' => 0x10,
                'key' => $type,
            )
        );

        $ret = array();
        while (true) {
            $item = $this->_recv();
            if (empty($item['key'])) {
                break;
            }
            $ret[$item['key']] = $item['body'];
        }
        return $ret;
    }

    public function append($key, $value)
    {
        // TODO: If the Memcached::OPT_COMPRESSION is enabled, the operation
        // should failed.
        $sent = $this->_send(array(
            'opcode' => 0x0e,
            'key' => self::PREFIX.$key,
            'value' => $value,
        ));
        $data = $this->_recv();
        if ($data['status'] == 0) {
            return TRUE;
        }

        return FALSE;
    }

    public function prepend($key, $value)
    {
        // TODO: If the Memcached::OPT_COMPRESSION is enabled, the operation
        // should failed.
        $sent = $this->_send(array(
            'opcode' => 0x0f,
            'key' => self::PREFIX.$key,
            'value' => $value,
        ));
        $data = $this->_recv();
        if ($data['status'] == 0) {
            return TRUE;
        }

        return FALSE;
    }

    public function getMulti(array $keys)
    {
        // TODO: from http://code.google.com/p/memcached/wiki/BinaryProtocolRevamped#Get,_Get_Quietly,_Get_Key,_Get_Key_Quietly
        //       Clients should implement multi-get (still important for reducing network roundtrips!) as n pipelined requests ...
        $list = array();

        foreach ($keys as $key) {
            $value = $this->get($key);
            if (false !== $value) {
                $list[$key] = $value;
            }
        }

        return $list;
    }


    protected $_options = array();

    public function setOption($key, $value)
    {
        $this->_options[$key] = $value;
    }

    /**
     * Set the memcache object to be a sessions handler
     *
     * Ex:
     * $m = new MemcacheSASL;
     * $m->addServer('xxx', 11211);
     * $m->setSaslAuthData('user', 'password');
     * $m->setSaveHandler();
     * session_start();
     * $_SESSION['hello'] = 'world';
     *
     * @access public
     * @return void
     */
    public function setSaveHandler()
    {
        session_set_save_handler(
            function ($savePath, $sessionName) { // open
            },
            function () { // close
            },
            function ($sessionId) { // read
                return $this->get($sessionId);
            },
            function ($sessionId, $data) { // write
                return $this->set($sessionId, $data);
            },
            function ($sessionId) { // destroy
                $this->delete($sessionId);
            },
            function ($lifetime) { // gc
            }
        );
    }
}
